iT邦幫忙

2022 iThome 鐵人賽

DAY 25
0

今天續奏昨天的未盡琴音


  • 還記得上篇的abstract classes抽象類別的定義嗎?類別中只要有一個抽象方法,該類別就是抽象類別

  • 和抽象類別相對,完全沒有抽象方法的,稱為concrete classes實體類別(或曰具體類別、具象類別、實際類別,甚而搞笑版的混凝土類別)。

  • 換句話說:

    • 實體類別:只包括一般的實體(即非抽象)方法(註1)。
    • 抽象類別:最少有一個抽象方法,實體方法則可有可無。
  • 繼承自抽象類別的子類別,必須實作父類別的所有抽象方法,否則子類別自己也變成抽象,無法建立實例。不過這裡的實作算是廣義的,只要定義出同名方法就算,內容給它pass...也行。

子類別一旦抽象,也不能建立實例

  • 抽象(父)類別設計:

    from abc import ABC, abstractmethod
    
    class Tree(ABC):    # 抽象方法
        def __init__(self, breed: str):
            self.__breed = breed
    
        @property
        def breed(self):
            return self.__breed
    
        @abstractmethod    # 抽象方法
        def provide_food(self):
            ...
    
        @abstractmethod    # 抽象方法
        def help_breathe(self):
            ...
    
        @abstractmethod    # 抽象方法
        def conserve_water(self):
            ...
    
  • 如果子類別完全不實作:

    class WeirdTree(Tree):
        ...
    
  • 試圖建立一棵WeirdTree時:

    try:
        tree = WeirdTree('dragon blood')
    except Exception as e:
        print(str(e))
    
  • 吃了個閉門羹:
    https://ithelp.ithome.com.tw/upload/images/20221010/20148485AcVkU5i0fr.png

  • 別以為子類別擴增其他方法,就可以「蒙混過關」。沒用的,父類別的抽象方法一個都不能少:

    class WeirdTree(Tree):
        def provide_food(self):     # 實作父類別的抽象方法(名稱)。
            ...                     # 內容則可以不實作出來。
    
        def help_breathe(self):     # 實作父類別的抽象方法(名稱)。
            ...
    
        # 少了conserve_water(self)
    
        def protect_land(self):     # 自己擴增的方法。
            print(f'I am a {__class__.__name__} {self.breed}.  I protect the land.')
    
  • 子類別實作了provide_food()help_breathe(),也新增一個父類別沒有的 protect_land()方法,卻漏掉conserve_water()。結果還是無法建立實例:
    https://ithelp.ithome.com.tw/upload/images/20221010/20148485goYdt7ho9S.png

  • 子類別唯有實作出父類別的所有抽象方法,成為實體類別,才可以建立自己的物件。這裡的「實作」,是只要定義方法名稱就算。signature相不相同無所謂,方法的內容也可以用pass或三個點(Ellipsis)略過。

    class WeirdTree(Tree):
        def provide_food(self, nutrients: dict):     # 必須有父類別的抽象方法。signature不同沒有關係,名稱相同就行。
            ...                     # 可以不實作內容。
    
        def help_breathe(self, freshing_index: int=-1):     # 必須有父類別的抽象方法。
            print(f'I am a {__class__.__name__} {self.breed}.  My air freshing index is {freshing_index}.')
    
        def conserve_water(self, conserving_index, improve_water_quality: bool):   # 必須有父類別的抽象方法。
            print(f'I am a {__class__.__name__} {self.breed}.  My water conserving index is {conserving_index}.  I {"can" if improve_water_quality else "cannot"} improve water quality.')
    
        def protect_land(self):     # 子類別擴充,增加自己的方法。
            print(f'I am a {__class__.__name__} {self.breed}.  I protect the land.')
    
  • 子類別終於可以建立的實例(物件了):
    https://ithelp.ithome.com.tw/upload/images/20221010/20148485II7DLJgxst.png

用super()呼叫抽象父類別

  • 使用abstract base classes模組製造出來的抽象類別,固然不能建立個體(instantiated),但可以經由其子類別以super()呼叫:
    from abc import ABC, abstractmethod
    
    class Tree(ABC):      # 這裡的「樹」採廣義,包括灌木(shrub)。
        def __init__(self, breed: str):
            self.__breed = breed
            print(f"I am the constructor of class {__class__.__name__}.  My breed is '{self.breed}'.")
    
        @property
        def breed(self):
            return self.__breed
    
        @abstractmethod
        def provide_food(self):
            ...
    
        @abstractmethod
        def help_breathe(self):   # abstract method原來也可以實作!
            print(f'I am an abstract method in abstract class {__class__.__name__}.')
    
        @abstractmethod
        def conserve_water(self):
            ...
    
        def provide_shelter(self):
            print(f'I am a concrete method in abstract class {__class__.__name__}.')
    
    
    class WeirdTree(Tree):
        def provide_food(self, nutrients: dict):
            ...
    
        def help_breathe(self, freshing_index: int=-1):
            ...
    
        def conserve_water(self, conserving_index, improve_water_quality: bool):
            ...
    
        def protect_land(self):  # 子類別擴充的方法。
            super()                     # 呼叫(抽象)父類別的constructor。
            super().provide_shelter()   # 呼叫(抽象)父類別的實體方法。
            super().help_breathe()      # 呼叫(抽象)父類別的抽象方法,可以嗎?
            print()
            print(f'I am a {__class__.__name__} {self.breed}.  I protect the land.')
    
  • 測試程式:
    try:
        tree = WeirdTree('bottle')
        tree.protect_land()
    except Exception as e:
        print(str(e))
    
  • 執行結果是:
    https://ithelp.ithome.com.tw/upload/images/20221010/201484858pxkkVJLqg.png
  • 從以上執行結果發現:
    • 可以在子類別的方法中用super()呼叫(抽象)父類別的建構子(constructor)。
    • 子類別可以用super().xxx()呼叫父類別的實體方法。
    • 奇怪的是:抽象方法原來也可以實作內容!上面的code,Tree類別中的help_breathe()就實作了內容。
    • 即使父類別的抽象方法實作了內容,子類別依然可以用super()呼叫。上例WeirdTree類別的protect_land()方法就呼叫了父類別的抽象方法super().help_breathe()
  • 對abstract base classes「抽象方法可以實作內容」這樣的行為模式,筆者無法斷定是否合理。當初此模組這樣設計,是否有甚麼特別考量,筆者也不了解,就暫不深究了。

Abstract Properties

  • 除了方法,properties也可以設為抽象:
    from abc import ABC, abstractmethod
    
    class Tree(ABC):
        def __init__(self, breed: str):   # constructor
            self.__breed = breed
            self.__water = -1
    
        @property
        def breed(self) -> str:   # 一般的property(getter)。
            return self.__breed
    
        @property
        def water(self) -> int:   # 一般的property(getter)。
            return self.__water
    
        @property                 
        def water_given(self) -> int:   # 一般的property(getter)。  
            return self.__water
    
        @water_given.setter             # water_given的setter property。
        def water_given(self, water: int):
            if water <= self.water_limit:
                self.__water = water
            else:
                raise ValueError(f"You can't water a {self.__breed} with {water} cc.")
    
        @property         # 注意兩個裝飾器的先後次序。
        @abstractmethod   # 這個就是abstract property。
        def water_limit(self):   # 意思是「給水量上限」。
            ...
    
        @abstractmethod 
        def watered(self):  # 意思是「澆了多少水給這棵樹」,也是abstract唷。
            ...
    
    
    class Hardwood(Tree):
        @property    # 要註明是property,否則視作一般方法,就不對了。
        def water_limit(self) -> int:   # 實作父類別的abstract property。
            return 3000
    
        def watered(self) -> None:   # 實作父類別的abstract method。
            ...   # 實際動作略。
            print(f"Watering a {self.breed} with {self.water} cc.") 
    
    
    class Conifer(Tree):
        @property  # 要註明property,否則視作一般方法,就不對了。
        def water_limit(self) -> int:   # 實作父類別的abstract property。
            return 2000
    
        def watered(self) -> None:   # 實作父類別的abstract method。
            ...   # 實際動作略。
            print(f"Watering a {self.breed} with {self.water} cc.")
    
  • 測試程式:
    try: 
        tree1 = Hardwood('maple')
        tree1.water_given = 3_000
        tree1.watered()
        print()
        tree2 = Conifer('cedar')
        tree2.water_given = 2_200  # 這行發揮property功能,攔阻超過上限的給水值。
        tree2.watered()
    except Exception as e:
        print(str(e))
    
  • 輸出:
    https://ithelp.ithome.com.tw/upload/images/20221010/20148485SbeuuGW6P1.png
  • 程式簡單說明:
    • water_limit(self)(水量上限)和watered(self)(受水)兩個都是抽象方法。
    • water_limit(self)既是抽象方法,也是property。要注意的是兩個裝飾器的先後次序,@property必須寫在@abstractmethod的上面,顛倒過來會引發錯誤。
    • 其餘程式碼還算清楚,就不多解釋了。

抽象類別的目的

  • 現在該回頭想一個基本問題:為甚麼物件導向的程式語言,要設計抽象方法和抽象類別?或者說,有了抽象類別,對系統有甚麼好處?
  • 這些問題的答案之一,以筆者粗淺理解,大概是這樣:
    • 現實世界的事物,總會有些共同且大致固定不變(或變動機會和幅度很小)的性質和行為,也有些較獨特或變動機會較大的性質和行為。資訊系統要模擬這種「有清有濁、有動有靜」的環境,就要先分析哪些是共同的性質,哪些是「地方事務」,甚麼部分善變,甚麼部分相對較為隱定不變。
    • 找出共同性質vs個別特性或常變與不變之後,可將共同或比較固定不變的部分抽出,設計成抽象類別,而非共同或比較常變的部分成為實體類別。
  • 例如樹木Tree會有哪些共同性質和行為呢?屬性大概就是樹種、樹齡、樹高等,行為(方法)大概會有生長、傳宗接代、生病和死亡等。而不同的樹會有不同行為,如闊葉樹會落葉、會開花,而針葉樹因市場價值較高(假設),會有交易行為。分析完後,各個類別大致可以這樣設計:
    from abc import ABC, abstractmethod
    
    class Tree(ABC):
        def __init__(self, breed: str, age: int, height: int):   # constructor
            self.__breed = breed
            self.__age = age
            self.__height = height
    
        @property
        def breed(self) -> str:
            return self.__breed
    
        @abstractmethod
        def grow(self):        # 生長(樹的共同行為)
            ...
    
        @abstractmethod        # 繁殖(樹的共同行為)
        def reproduce(self):
            ...
    
        @abstractmethod        # 生病(樹的共同行為)
        def get_sick(self):
            ...
    
        @abstractmethod        # 死亡(樹的共同行為)
        def die(self):
            ...
    
    
    class Hardwood(Tree):   # Inherited from Tree
        ...   # construtor略
    
        def grow(self):        # 實作父類別的生長行為。
            print(f'{__class__.__name__} {self.breed} is growing.')
    
        def reproduce(self):   # 實作父類別的繁殖行為。
            print(f'{__class__.__name__} {self.breed} is reproducing.')
    
        def get_sick(self):     # 實作父類別的生病行為。
            print(f'{__class__.__name__} {self.breed} is getting sick.')
    
        def die(self):          # 實作父類別的死亡行為(假設死亡也有「行為」)。
            print(f'{__class__.__name__} {self.breed} is dying.')
    
        def defoliate(self):    # 自己的行為:落葉
            print(f'{__class__.__name__} {self.breed} is defoliating.')
    
        def bloom(self):        # 自己的行為:開花
            print(f'{__class__.__name__} {self.breed} is blooming.')
    
    
    class Conifer(Tree):   # Inherited from Tree
        ...   # construtor略
    
        def grow(self):        
            print(f'{__class__.__name__} {self.breed} is growing.')
    
        def reproduce(self):
            print(f'{__class__.__name__} {self.breed} is reproducing.')
    
        def get_sick(self):
            print(f'{__class__.__name__} {self.breed} is getting sick.')
    
        def die(self):
            print(f'{__class__.__name__} {self.breed} is dying.')
    
        def sell(self):        # 自己的行為:交易
            print(f'{__class__.__name__} {self.breed} is being sold.')
    
  • 驗證:
    tree1 = Hardwood('oak', 50, 100)
    tree1.breed
    tree1.grow()
    tree1.reproduce()
    tree1.get_sick()
    tree1.die()
    tree1.defoliate()
    tree1.bloom()
    
    print()
    tree2 = Conifer('cedar', 96, 1_500)
    tree2.breed
    tree2.grow()
    tree2.reproduce()
    tree2.get_sick()
    tree2.die()
    tree2.sell()
    
  • 輸出:
    https://ithelp.ithome.com.tw/upload/images/20221010/20148485K7YZ5X2YOp.png

抽象類別的介紹到此為止。明天講繼承的最後一個主題,也可能是繼承的最後一篇。


註1: concrete methods當然也可譯為「具體方法」,筆者捨「具體」而取「實體」,個人喜好耳。


上一篇
ABC: 談談Abstract Classes
下一篇
繼承vs組合
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言